#!/usr/bin/python

# Project 2
# bruinbase

import os, sys, subprocess, getopt, re, csv, shutil;
from subprocess import Popen, PIPE, STDOUT,call;
from collections import defaultdict;
from shutil import rmtree;
                         
# IMPORTANT IF THIS SCRIPT IS NOT RUN FROM ITS OWN DIRECTORY
#	saves caller's directory, changes to script's directory
#	at the end of script, changes back to caller's directory
caller_dir = os.getcwd()
script_dir = sys.path[0]
os.chdir(script_dir)

from helper_functions import *;    	# import helper functions from helper_functions.py
from config import *;        		# loads global variables from config.py
     

################ Global Variables ####################

USAGE = """UCLA-CS143 - Project 2 Grading Script

USAGE: main.py -s <submission_dir> -d <mysql_DB> -u <mysql_user> -id <student-id>
	--help
		Displays input options
	-s, --submissions <enter-location-here>
		(Optional)Provide location of student submission directory
	-n, --id <student-id>
		(Optional)System will test only student provided
	-t, --timeout <seconds>
		(Optional)Time out interval
	[ To view/change default values, please see the configuration file (config.py). ]

"""

#CHANGE GLOBAL VARIABLES BASED ON COMMAND-LINE INPUT
try:
	options, remainder = getopt.gnu_getopt(sys.argv[1:], 's:n:t:', ['submissions=','id=','timeout=','help'])
except getopt.GetoptError, err:
	print str(err)
	print USAGE
	sys.exit(2)

for opt, arg in options:
	if opt in ('-s', '--submissions'):
		print "Changing submission directory to %s" % (arg)
		
		# drop forward slash at the end of string if present
		submission_dir = re.sub('/$', '', arg) 
		
	elif opt in ('-n', '--id'):
		student_id = arg
	elif opt in ('-t', '--timeout'):
		timeout = arg
	elif opt == '--help':
		print USAGE
		sys.exit(2)
	else:
		print "option %s with value %s is not recognized."	% (opt, arg)
		assert False, "unhandled option"

################ END - Global Variables ####################                

                                                                             
# CLEAN UP
# clean up processing folder
if (os.path.exists(processing_dir)):
	shutil.rmtree(processing_dir)
os.mkdir(processing_dir)

if (os.path.exists(basefile_dir)):
	shutil.rmtree(basefile_dir)
os.mkdir(basefile_dir)	
	  
# PREPARE FOLDERS
if (not os.path.exists(log_dir)):
	os.mkdir(log_dir)
	  
if (not os.path.exists(results_dir)):
	os.mkdir(results_dir)
	
if (not os.path.exists(grading_dir)):
	os.mkdir(grading_dir)
                                         
		             
    
# unzip bruinbase
os.system("unzip -d "+ basefile_dir + " " + basefile)


# ORGANIZE STUDENT SUBMISSION
# copy files from a,b,c,d subfolder in submission folder into desire location in order 
for subfolder in submissions_subfolder_list:

	submission_cvs_file = submission_dir + '/' + subfolder + '/' + submission_data_file
	
	log = open(submission_cvs_file, 'r') or sys.exit("Unable to open csv file: " + submission_cvs_file)
	log_lines = set(log.read().splitlines())
	# read from csv file - line by line
	for line in log_lines:
		if(len(line.strip()) != 0):
			col = line.split(',')
			folder_sid = re.sub('"','',col[0])
			  
			# copy file from source to temporary processing folder
			if(not os.path.exists(processing_dir+'/'+folder_sid)):
				os.system("mkdir -p " + processing_dir + '/' + folder_sid )
			os.system("cp -r " + submission_dir + '/' + subfolder + '/' + folder_sid + '/* ' + processing_dir + '/' + folder_sid)
			                                                      
			# copy file from source to temporary processing folder if there are partner name
			if(re.match('^[0-9]{9}$',re.sub('"','',col[2]))):
				partner_sid = re.sub('"','',re.sub('"','',col[2]))
				if(not os.path.exists(processing_dir+'/'+partner_sid)):
					os.system("mkdir -p " + processing_dir + '/' + partner_sid )
				os.system("cp -r " + submission_dir + '/' + subfolder + '/' + folder_sid + '/* ' + processing_dir + '/' + partner_sid)
	log.close()                                                                                                   
# DONE - ORGANIZE STUDENT SUBMISSION  


# LOAD TEST CASES
testcases_index = defaultdict(dict)
testcases_sol_index = defaultdict(set)
testcases_no_index = defaultdict(dict)
testcases_sol_no_index = defaultdict(set)

testcases_fh = open(testcase_dir + '/' + testcase_file, 'r') or sys.exit("Unable to open testcase file: " + testcase_dir + '/' + testcase_file)
testcases_lines = set(testcases_fh.read().splitlines())
# read from csv file - line by line
for line in testcases_lines:
	if(len(line.strip()) != 0):
		# example : 1||LOAD||XS||LOAD xsmall FROM 'xsmall.del' WITH INDEX||  
		cols = line.split('||')
		
		case = { 'type' : cols[2] ,
				'category' : cols[3],
				'query' : cols[4],
				'query2' : cols[5]
				} 
		
		if(cols[1] == 'I'):		
			testcases_index[cols[0]] = case
		elif(cols[1] == 'NI'):
			testcases_no_index[cols[0]] = case
                              
		# load answer set if the test case has category 'TEST'
		# one answer set per file
		if(cols[2] == "TEST"):
			sol_file = str(cols[0]) + "_" + str(cols[1]) + "_" + str(cols[3]) + "_sol.txt"
			sol_fh = open(testcase_dir + '/' + sol_file, 'r') or sys.exit("Unable to open testcase solution file: " + testcase_dir + '/' + sol_file)
			sol_set = set(sol_fh.read().splitlines())
			if(cols[1] == 'I'):		
				testcases_sol_index[cols[0]] = sol_set
			elif(cols[1] == 'NI'):
				testcases_sol_no_index[cols[0]] = sol_set
			sol_fh.close()
		     	
testcases_fh.close()
# DONE - LOAD TEST CASES           


# LOAD TESTCASES SOLUTION - SHARED FILE
testcases_sol_sf_index = defaultdict(dict)
testcases_sol_sf_no_index = defaultdict(dict)

testcases_sol_fh = open(testcase_dir + '/' + testcase_sol_shared_file, 'r') or sys.exit("Unable to open testcase solution file: " + testcase_dir + '/' + testcase_sol_shared_file)
testcases_sol_lines = set(testcases_sol_fh.read().splitlines())
for line in testcases_sol_lines:
	if(len(line.strip()) != 0):
		# example : [Case num]||[Index]||[PAGE READ MIN]||[PAGE READ MAX]||[PAGE READ MIN (Partial pt)]||[PAGE READ MAX (Partial pt)]||[MIN TIME OUT]                                            
		  
		cols = line.split('||')
		sol = { 'pg_min' : cols[2] ,
				'pg_max' : cols[3],
				'pg_min_partial' : cols[4] ,
				'pg_max_partial' : cols[5],
				'time_min' : cols[6]
				} 
		
		if(cols[1] == 'I'):
			testcases_sol_sf_index[cols[0]] = sol
		elif(cols[1] == 'NI'):
			testcases_sol_sf_no_index[cols[0]] = sol
                              
testcases_sol_fh.close()

# DONE - LOAD TESTCASES SOLUTION - SHARED FILE

# LOAD TESTCASES SOLUTION - LOAD TIMEOUT CONFIG FILE (load_timeout.txt)
load_timeout_config = defaultdict(float)

testcases_load_timeout_fh = open(testcase_dir + '/' + testcase_sol_load_timeout_file, 'r') or sys.exit("Unable to open testcase solution file: " + testcase_dir + '/' + testcase_sol_load_timeout_file)
testcases_load_timeout_lines = set(testcases_load_timeout_fh.read().splitlines())
for line in testcases_load_timeout_lines:
	if(len(line.strip()) != 0):
		# example : [Category]||[MIN TIME OUT]                                            
		cols = line.split('||')
		load_timeout_config[str(cols[0])] = float(cols[1])
                              
testcases_load_timeout_fh.close()

# DONE - LOAD TESTCASES SOLUTION - LOAD TIMEOUT CONFIG FILE (load_timeout.txt)   
   

# START GRADING PROCESS
start_time = time.time()
print "=== Starting Auto Grading ===" 
# store the list of student submission
print "=== Checking Student Submissions ==="
student_list = []
if(student_id == 0):
	student_list = listdirs(processing_dir)
	student_list.sort()
else:
	student_list.append(student_id)
print "Number of directories (submissions) found: ", len(student_list)

# CREATE FILE HANDLER for GRADEBOOK(OUTPUT)
resultsFile = open(result_file, 'w') or sys.exit("Unable to open output Gradebook file: " + result_file)
resultsFile.write(header)                                                                               
      

print "=== Starting auto grading ==="
for sid in student_list:
	print "== " + sid + " START =="
	current_start = time.time()
	
	os.chdir(script_dir)
	# LOCAL VARIABLES
	points_array = []
	# 23 test cases for grading
	for i in range(0,23):
		points_array.append(int(0))
	comments = ""
	case = {}
	
	# CREATE FILE HANDLER for INDIVIDUAL LOG(DETAIL STUDENT RESULT)
	log_file = log_dir + "/" + sid + ".log"
	logsFile = open(log_file, 'w') or sys.exit("Unable to open student log file: " + log_file)   
	logsFile.write(time.strftime('%c'))
	# CLEAN UP GRADING FOLDER 
	if (os.path.exists(grading_dir)):
		shutil.rmtree(grading_dir)
	os.mkdir(grading_dir)

	# PREPARE GRADING FOLDER   
	# copy data to grading folder
	os.system("cp -f "+ basefile_dir + "/* " + grading_dir)
	os.system("cp -f "+ data_dir + "/*.del " + grading_dir)
	os.system("cp -f "+ processing_dir + "/" + sid + "/* " + grading_dir)
	os.system("cp -f "+ reload_file + " " + grading_dir)

	os.chdir(grading_dir)
	comp_time = time.time()
	(to_err, stdout, stderr, retcode) = runCmd("make", 0)
	if(to_err == 1 or retcode <> 0):
		# COMPILATION FAILURE
		comments += "COMPILATION FAILURE (ABORT GRADING); "
		print "= Bruinbase compiled with error ="
		logsFile.write("= Bruinbase compiled with error =\n")
		write_gradebook(resultsFile,sid,points_array,comments)
		# skip current student grading due to compilation failure
		continue
    
	print "= Bruinbase compiled successfully ="
	logsFile.write("= Bruinbase compiled successfully =\n")
	
	print "= Bruinbase Compilation Time :", GetInHMS(int(time.time() - comp_time))," =" 
	
	sys.stdout.flush()
	
                                                                         
	# GRADING TESTCASES WITH INDEX
	print "= Grading Testcases with Index ="
	logsFile.write("= Grading Testcases with Index =\n")
	for case_num in range(0,len(testcases_index)):
		
		#current len(testcases_index) = 21 -> 0 - 20
		
		# local variables
		answer_set = set()
		cmp_set = set()
		sol_set = set()
		process_time = -1.0
		pageread = -1                                
		case = testcases_index[str(case_num)]
		query_timeout = timeout
		
		# logging testcases
		logsFile.write("------------------------------------------\n") 
		logsFile.write("Testcase number [" + str(case_num) + "]\n")
		logsFile.write("Test Query: [" + case['query'] + "]\n")
		if(len(case['query2']) > 0):
			logsFile.write("Comparing Query: [" + case['query2'] + "]\n")
				
        # write the query to tmp.sql for grading
		tmpSQL = open(tmpSQL_file, 'w') or sys.exit("Unable to open tmp sql file: " + tmpSQL_file)
		tmpSQL.write(case["query"] + "\n") 
		tmpSQL.close()   	   
		# runCmd with student's bruinbase 
		cmd = "./bruinbase < " + tmpSQL_file
		
		# use grader defined timeout config
		if(case['type'] == "LOAD"):
			query_timeout = load_timeout_config[str(case['category'])]
			
		(to_err, stdout, stderr, retcode) = runCmd(cmd, query_timeout)
		
		logsFile.write("-------\n")
		
		# Grading Process
		# time out error
		if( to_err == 1 ):
			comments += str(case_num) + "-TO; "
			logsFile.write("Timeout error\n")
		# query executed with error	
		elif( retcode <> 0 ):
			comments += str(case_num) + "-Err["+ str(retcode) +"]; "
			logsFile.write("Query failed with return code" + str(retcode) + "\n")			
		else:
			
			# Type - LOAD
			if(case['type'] == "LOAD"):
				               
				# TODO - grad loading - check if data is present with file size > 1
				try:              
					idx_file = str(case["category"])+".idx"
					tbl_file = str(case["category"])+".tbl"
					
					table_load_ck1 = os.path.getsize(idx_file)
					table_load_ck2 = os.path.getsize(tbl_file)
					if( os.path.exists(idx_file) and 
						os.path.exists(tbl_file) and 
						table_load_ck1 > 0 and
						table_load_ck2 > 0 ):
						
						points_array[case_num] += load_pt
						
						logsFile.write("LOAD SUCCESS idx filesize:" + str(table_load_ck1) + ", tbl filesize:" + str(table_load_ck2) + "\n")
				except OSError, e:
					comments += str(case_num) + "-LD; "
				          
			# Type - TEST
			elif(case['type'] == "TEST"):
				
				# parsing stdout, stderr
				answer_set = parse_returning_set(str(stdout))
				(process_time,pageread) = parse_processing_line(str(stderr))

				if(len(case['query2']) > 0):
					# write the query to tmp.sql for grading
					tmpSQL = open(tmpSQL_file, 'w') or sys.exit("Unable to open tmp sql file: " + tmpSQL_file)
					tmpSQL.write(case["query2"] + "\n") 
					tmpSQL.close()   	   
					# runCmd with student's bruinbase 
					cmd = "./bruinbase < " + tmpSQL_file
					(to_err, stdout, stderr, retcode) = runCmd(cmd, timeout)
					
					cmp_set = parse_returning_set(str(stdout))

				# TO DO - Grading answer set , processing time , and pageread
				# 1) grade answer set
				sol_set = testcases_sol_index[str(case_num)]
				if(len(answer_set ^ sol_set) > 0):
					logsFile.write("SOLUTION: " + '|'.join(sol_set) + "\nSTUDENT ANSWER: "+ '|'.join(answer_set) + "\n")
					# Give Partial Pts If comparing query gives the same set as answer set
					if(len(cmp_set) > 0 and len(answer_set ^ cmp_set) == 0):
						points_array[case_num] += answer_pt
						logsFile.write("***comparing query returns same set (partial pt)\n")
					else:
						# 2012 exception - free point on answer set for case num 11 : SELECT * FROM medium WHERE key = 3936 AND value > 'Superman'
						if(case_num == 11):
							#comments += str(case_num) + "-WA; "
							points_array[case_num] += answer_pt 
						else:
							comments += str(case_num) + "-WA; "
						
				else:
					points_array[case_num] += answer_pt
					logsFile.write("Correct answer set!!!\n")

				# 2) grade pageread
				sol_sf = testcases_sol_sf_index[str(case_num)] 
				logsFile.write("pageread: "+str(pageread)+"\n")
				if(pageread == -1):
					comments += str(case_num) + "-PR; " 
					logsFile.write("Cannot parse page read!!!\n")
				elif(pageread >= int(sol_sf['pg_min']) and pageread <= int(sol_sf['pg_max'])):
					points_array[case_num] += pageread_pt
					logsFile.write("Correct page read!!!\n")	
				elif( len(sol_sf['pg_min_partial']) > 0 and
					len(sol_sf['pg_max_partial']) > 0 and
					pageread >= int(sol_sf['pg_min_partial']) and
					pageread <= int(sol_sf['pg_max_partial'])):
					# partial point
					points_array[case_num] += pageread_partial_pt
					comments += str(case_num) + "-PRP; "
					logsFile.write("Partially correct page read!!!\n")
				else:
					# PROVIDE EXCEPTION GRADING HERE - added as config? - Decision: no, due to small amount of exception 
					#15: 19 ~ 27  
					#18: 30 ~ 60
					#19: 1200 ~ 1600
					if((case_num == 15 and pageread >= 19 and pageread <= 27) or
						(case_num == 18 and pageread >= 30 and pageread <= 60) or
						(case_num == 19 and pageread >= 1200 and pageread <= 1600)):
						points_array[case_num] += pageread_pt
						logsFile.write("Correct page read!!!\n")
					else:
						comments += str(case_num) + "-PR; "
						logsFile.write("Wrong page read!!!\n")
						

				logsFile.write("process time: "+str(process_time)+"\n")
				# 3) grade processing time
				if(process_time == -1.0 ):
					comments += str(case_num) + "-PT; " 
					logsFile.write("Cannot parse process time!!!\n")
				elif(process_time <= float(sol_sf['time_min'])):
					points_array[case_num] += proc_time_pt
					logsFile.write("Process time passed!!!\n")
				else:
					comments += str(case_num) + "-PT; "
					logsFile.write("Process time not passed!!!\n")
				
			print "case[" + str(case_num) + "] - " + str(points_array[case_num]) + "pt"
			logsFile.write("case[" + str(case_num) + "] - " + str(points_array[case_num]) + "pt\n")
				 
  					
	# GRADING TESTCASES WITHOUT INDEX
	print "= Grading Testcases without Index ="
	logsFile.write("= Grading Testcases without Index =\n")
	
	print "= Removing Index ="
	logsFile.write("= Removing Index =\n")
	os.system("rm -f *.idx *.tbl")
	
	print "= Reloading Table without Index ="
	logsFile.write("= Reloading Table without Index =\n")
	cmd = "./bruinbase < " + reload_file
	(to_err, stdout, stderr, retcode) = runCmd(cmd, timeout)
	
	for case_num in range(0,len(testcases_no_index)):
			
			
		# local variables
		answer_set = set()
		cmp_set = set()
		sol_set = set()
		process_time = -1.0
		pageread = -1                                
		case = testcases_no_index[str(case_num)]
		case_num_real = case_num + len(testcases_index)

		# logging student grading
		logsFile.write("------------------------------------------\n")
		logsFile.write("Testcase number [" + str(case_num) + "]\n")
		logsFile.write("Test Query: [" + case['query'] + "]\n")
		if(len(case['query2']) > 0):
			logsFile.write("Comparing Query: [" + case['query2'] + "]\n")		

		
        # write the query to tmp.sql for grading
		tmpSQL = open(tmpSQL_file, 'w') or sys.exit("Unable to open tmp sql file: " + tmpSQL_file)
		tmpSQL.write(case['query'] + "\n")
		tmpSQL.close()
        
		# runCmd with student's bruinbase           
		cmd = "./bruinbase < " + tmpSQL_file
		(to_err, stdout, stderr, retcode) = runCmd(cmd, timeout)

		logsFile.write("-------\n")

		# Grading Process
		# time out error
		if( to_err == 1 ):
			comments += str(case_num_real) + "-TO; "
			logsFile.write("Timeout error\n")
		# query executed with error	
		elif( retcode <> 0 ):
			comments += str(case_num_real) + "-Err["+ str(retcode) +"]; "
			logsFile.write("Query failed with return code" + str(retcode) + "\n")			
		else:
			if(case['type'] == "TEST"):

				# parsing stdout, stderr
				answer_set = parse_returning_set(str(stdout))
				(process_time,pageread) = parse_processing_line(str(stderr))

				if(len(case['query2']) > 0):
					# write the query to tmp.sql for grading
					tmpSQL = open(tmpSQL_file, 'w') or sys.exit("Unable to open tmp sql file: " + tmpSQL_file)
					tmpSQL.write(case["query2"] + "\n") 
					tmpSQL.close()   	   
					# runCmd with student's bruinbase 
					cmd = "./bruinbase < " + tmpSQL_file
					(to_err, stdout, stderr, retcode) = runCmd(cmd, timeout)

					cmp_set = parse_returning_set(str(stdout))

				# TO DO - Grading answer set , processing time , and pageread
				# 1) grade answer set
				sol_set = testcases_sol_no_index[str(case_num)]
				if(len(answer_set ^ sol_set) > 0):
					logsFile.write("SOLUTION: " + '|'.join(sol_set) + "\nSTUDENT ANSWER: "+ '|'.join(answer_set) + "\n")
					# Give Partial Pts If comparing query gives the same set as answer set
					if(len(cmp_set) > 0 and len(answer_set ^ cmp_set) == 0):
						points_array[case_num_real] += answer_pt
						logsFile.write("***comparing query returns same set (partial pt)\n")
					else:
						comments += str(case_num_real) + "-WA; "

				else:
					points_array[case_num_real] += answer_pt
					logsFile.write("Correct answer set!!!\n")

				# 2) grade pageread
				sol_sf = testcases_sol_sf_no_index[str(case_num)] 
				logsFile.write("pageread: "+str(pageread)+"\n")
				if(pageread == -1):
					comments += str(case_num_real) + "-PR; " 
					logsFile.write("Cannot parse page read!!!\n")
				elif(pageread >= int(sol_sf['pg_min']) and pageread <= int(sol_sf['pg_max'])):
					points_array[case_num_real] += pageread_pt
					logsFile.write("Correct page read!!!\n")	
				elif( len(sol_sf['pg_min_partial']) > 0 and
					len(sol_sf['pg_max_partial']) > 0 and
					pageread >= int(sol_sf['pg_min_partial']) and
					pageread <= int(sol_sf['pg_max_partial'])):
					# partial point
					points_array[case_num_real] += pageread_partial_pt
					comments += str(case_num_real) + "-PRP; "
					logsFile.write("Partially correct page read!!!\n")
				else:
					# NO EXCEPTION
					comments += str(case_num_real) + "-PR; "
					logsFile.write("Wrong page read!!!\n")


				logsFile.write("process time: "+str(process_time)+"\n")
				# 3) grade processing time
				if(process_time == -1.0 ):
					comments += str(case_num_real) + "-PT; " 
					logsFile.write("Cannot parse process time!!!\n")
				elif(process_time <= float(sol_sf['time_min'])):
					points_array[case_num_real] += proc_time_pt
					logsFile.write("Process time passed!!!\n")
				else:
					comments += str(case_num_real) + "-PT; "
					logsFile.write("Process time not passed!!!\n")

			print "case[" + str(case_num) + "] - " + str(points_array[case_num_real]) + "pt"
			logsFile.write("case[" + str(case_num) + "] - " + str(points_array[case_num_real]) + "pt\n")
	
	write_gradebook(resultsFile,sid,points_array,comments)
	print "[Time: ", round(time.time() - current_start,2) , "secs]"
	print "== " + sid + " DONE =="
	logsFile.close() 
	sys.stdout.flush()
	# done of test loop

#done of student loop
resultsFile.close() 
print "=== Project 2 grading is completed ===" 
total_time = int(time.time() - start_time)
print "Auto-Grader Execution Time: ", GetInHMS(total_time)
print "The result file written to ", result_file


# switch back to the caller's directory
os.chdir(caller_dir)
